Popular Searches
Popular Course Categories
Popular Courses

Build a Flutter Chat Application

Build a Flutter Chat Application

Flutter Practical Projects


Build a Flutter Chat Application


A Flutter Chat Application is a practical project that demonstrates how to build a real-time messaging application using Flutter and Dart. A chat application can include user registration, login, user profiles, one-to-one messaging, chat lists, message timestamps, online/offline status, typing indicators, message notifications, image sharing, and message history.


This project helps learners understand how Flutter widgets, navigation, asynchronous programming, state management, backend services, authentication, databases, and real-time data can work together to create a complete application.




1. Objectives of the Chat Application



  • Create a complete chat application using Flutter.

  • Create user registration and login screens.

  • Display a list of users or conversations.

  • Create one-to-one chat functionality.

  • Send and receive text messages.

  • Display message timestamps.

  • Show sent and received messages differently.

  • Manage chat state.

  • Store messages in a backend database.

  • Implement real-time message updates.

  • Show online and offline status.

  • Display typing indicators.

  • Implement message notifications.

  • Support image or file sharing.

  • Implement message deletion where required.

  • Display chat history.

  • Handle loading, empty, and error states.

  • Create a responsive chat interface.




2. Technologies Used











TechnologyPurpose
FlutterBuild the cross-platform user interface.
DartProgramming language used for Flutter development.
Firebase AuthenticationHandle user registration and authentication.
Cloud FirestoreStore and synchronize chat messages and user data.
Firebase StorageStore images and other uploaded files.
Firebase Cloud MessagingSend push notifications.
ProviderOne possible approach for managing shared application state.



3. Main Features



  • Splash Screen

  • Onboarding Screen

  • User Registration

  • User Login

  • User Profile

  • Chat List

  • One-to-One Chat

  • Real-Time Messages

  • Message Timestamps

  • Online Status

  • Typing Indicator

  • Unread Message Count

  • Message Notifications

  • Image Sharing

  • File Sharing

  • Message Deletion

  • Chat Search

  • User Search

  • Block or Report User

  • Logout




4. Chat Application Flow


Launch App
    ↓
Splash Screen
    ↓
Check Authentication
    ↓
Login / Register
    ↓
Home / Chat List
    ↓
Select User
    ↓
Chat Screen
    ↓
Type Message
    ↓
Send Message
    ↓
Store Message
    ↓
Real-Time Message Update
    ↓
Notification
    ↓
Chat History



5. Recommended Project Structure


lib/
├── main.dart
├── models/
│   ├── user_model.dart
│   ├── message_model.dart
│   └── chat_model.dart
├── screens/
│   ├── splash_screen.dart
│   ├── login_screen.dart
│   ├── register_screen.dart
│   ├── home_screen.dart
│   ├── chat_list_screen.dart
│   ├── chat_screen.dart
│   └── profile_screen.dart
├── services/
│   ├── auth_service.dart
│   ├── chat_service.dart
│   ├── notification_service.dart
│   └── storage_service.dart
├── providers/
│   ├── auth_provider.dart
│   └── chat_provider.dart
├── widgets/
│   ├── chat_tile.dart
│   ├── message_bubble.dart
│   ├── user_avatar.dart
│   └── typing_indicator.dart
└── utils/
    └── constants.dart

Separating models, screens, services, providers, and reusable widgets keeps the application organized and easier to maintain.




6. Create the Flutter Project


Create a new Flutter project using the terminal:


flutter create chat_application
cd chat_application
flutter run



7. Add Required Packages


For a Firebase-based chat application, commonly required packages can include:


flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutter pub add firebase_storage
flutter pub add firebase_messaging

Additional packages can be added for features such as image selection, local notifications, or state management.




8. Configure Firebase


Firebase can be used as the backend for authentication, real-time database functionality, file storage, and notifications.


The general setup flow is:



  1. Create a Firebase project.

  2. Register the Android and/or iOS application.

  3. Configure Firebase for the Flutter project.

  4. Add the required Firebase packages.

  5. Initialize Firebase in the Flutter application.

  6. Enable Authentication.

  7. Create a Firestore database.

  8. Configure Firebase Storage if media sharing is required.

  9. Configure Firebase Cloud Messaging if push notifications are required.




9. Initialize Firebase


A typical Flutter application initializes Firebase before starting the application.


import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  runApp(const ChatApplication());
}




10. Create the Main Application


class ChatApplication extends StatelessWidget {
  const ChatApplication({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Chat',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
        ),
        useMaterial3: true,
      ),
      home: const LoginScreen(),
    );
  }
}




11. User Model


The user model represents information about a registered chat user.


class UserModel {
  final String id;
  final String name;
  final String email;
  final String? photoUrl;
  final bool isOnline;

  UserModel({
    required this.id,
    required this.name,
    required this.email,
    this.photoUrl,
    this.isOnline = false,
  });

  factory UserModel.fromMap(
    Map data,
    String id,
  ) {
    return UserModel(
      id: id,
      name: data['name'] ?? '',
      email: data['email'] ?? '',
      photoUrl: data['photoUrl'],
      isOnline: data['isOnline'] ?? false,
    );
  }

  Map toMap() {
    return {
      'name': name,
      'email': email,
      'photoUrl': photoUrl,
      'isOnline': isOnline,
    };
  }
}




12. Message Model


Each chat message can contain a sender ID, receiver ID, message text, timestamp, and message type.


class MessageModel {
  final String id;
  final String senderId;
  final String receiverId;
  final String text;
  final DateTime timestamp;
  final String type;

  MessageModel({
    required this.id,
    required this.senderId,
    required this.receiverId,
    required this.text,
    required this.timestamp,
    this.type = 'text',
  });

  factory MessageModel.fromMap(
    Map data,
    String id,
  ) {
    return MessageModel(
      id: id,
      senderId: data['senderId'] ?? '',
      receiverId: data['receiverId'] ?? '',
      text: data['text'] ?? '',
      timestamp: data['timestamp'].toDate(),
      type: data['type'] ?? 'text',
    );
  }

  Map toMap() {
    return {
      'senderId': senderId,
      'receiverId': receiverId,
      'text': text,
      'timestamp': timestamp,
      'type': type,
    };
  }
}




13. Chat Model


A chat model can represent a conversation between two users.


class ChatModel {
  final String id;
  final String otherUserId;
  final String lastMessage;
  final DateTime? lastMessageTime;
  final int unreadCount;

  ChatModel({
    required this.id,
    required this.otherUserId,
    required this.lastMessage,
    this.lastMessageTime,
    this.unreadCount = 0,
  });
}




14. Firebase Authentication


Firebase Authentication can be used to register and authenticate users.


import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth =
      FirebaseAuth.instance;

  Future register(
    String email,
    String password,
  ) async {
    return await _auth
        .createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
  }

  Future login(
    String email,
    String password,
  ) async {
    return await _auth
        .signInWithEmailAndPassword(
      email: email,
      password: password,
    );
  }

  Future logout() async {
    await _auth.signOut();
  }
}




15. Registration Screen


The registration screen allows a new user to create an account.


final emailController = TextEditingController();
final passwordController = TextEditingController();
final nameController = TextEditingController();

TextField(
  controller: nameController,
  decoration: const InputDecoration(
    labelText: 'Name',
    prefixIcon: Icon(Icons.person),
  ),
)

TextField(
  controller: emailController,
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email',
    prefixIcon: Icon(Icons.email),
  ),
)

TextField(
  controller: passwordController,
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
    prefixIcon: Icon(Icons.lock),
  ),
)




16. Login Screen


class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State createState() =>
      _LoginScreenState();
}

class _LoginScreenState
    extends State {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  bool isLoading = false;

  Future login() async {
    setState(() {
      isLoading = true;
    });

    try {
      await AuthService().login(
        emailController.text.trim(),
        passwordController.text.trim(),
      );

      if (!mounted) return;

      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (_) => const HomeScreen(),
        ),
      );
    } catch (error) {
      if (!mounted) return;

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            'Login failed: $error',
          ),
        ),
      );
    } finally {
      if (mounted) {
        setState(() {
          isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
              ),
            ),
            const SizedBox(height: 20),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isLoading ? null : login,
                child: isLoading
                    ? const CircularProgressIndicator()
                    : const Text('Login'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}




17. Form Validation


Form validation prevents invalid or incomplete information from being submitted.


final formKey = GlobalKey();

Form(
  key: formKey,
  child: TextFormField(
    validator: (value) {
      if (value == null ||
          value.trim().isEmpty) {
        return 'Email is required';
      }

      if (!value.contains('@')) {
        return 'Enter a valid email';
      }

      return null;
    },
  ),
)




18. Firestore Database Structure


A simple chat application can organize data using users, chats, and messages.


users/
  userId/
    name
    email
    photoUrl
    isOnline
    lastSeen

chats/
  chatId/
    participants
    lastMessage
    lastMessageTime

chats/
  chatId/
    messages/
      messageId/
        senderId
        receiverId
        text
        timestamp
        type




19. Users Collection


The users collection can store profile and presence information.


FirebaseFirestore.instance
    .collection('users')
    .doc(userId)
    .set({
  'name': name,
  'email': email,
  'photoUrl': photoUrl,
  'isOnline': true,
  'lastSeen': FieldValue.serverTimestamp(),
});



20. Send a Message


A message can be stored inside a chat's messages subcollection.


Future sendMessage({
  required String chatId,
  required String senderId,
  required String receiverId,
  required String text,
}) async {
  if (text.trim().isEmpty) {
    return;
  }

  await FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .add({
    'senderId': senderId,
    'receiverId': receiverId,
    'text': text.trim(),
    'timestamp': FieldValue.serverTimestamp(),
    'type': 'text',
  });
}




21. Chat Service


Moving database operations into a service class keeps UI widgets cleaner.


class ChatService {
  final FirebaseFirestore _firestore =
      FirebaseFirestore.instance;

  Future sendMessage({
    required String chatId,
    required String senderId,
    required String receiverId,
    required String text,
  }) async {
    await _firestore
        .collection('chats')
        .doc(chatId)
        .collection('messages')
        .add({
      'senderId': senderId,
      'receiverId': receiverId,
      'text': text,
      'timestamp':
          FieldValue.serverTimestamp(),
      'type': 'text',
    });
  }
}




22. Listen to Messages in Real Time


A chat application needs to update the interface when a new message arrives. Firestore provides real-time listeners that can be consumed using a stream.


Stream getMessages(
  String chatId,
) {
  return FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .orderBy('timestamp')
      .snapshots();
}



23. Display Messages Using StreamBuilder


StreamBuilder can rebuild the UI when the message stream changes.


StreamBuilder(
  stream: ChatService().getMessages(chatId),
  builder: (context, snapshot) {
    if (snapshot.connectionState ==
        ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return const Center(
        child: Text(
          'Unable to load messages',
        ),
      );
    }

    final messages =
        snapshot.data?.docs ?? [];

    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, index) {
        final data =
            messages[index].data()
                as Map;

        return MessageBubble(
          text: data['text'] ?? '',
          isMe:
              data['senderId'] == currentUserId,
        );
      },
    );
  },
)




24. Message Bubble


Sent and received messages can use different alignment and styling.


class MessageBubble extends StatelessWidget {
  final String text;
  final bool isMe;

  const MessageBubble({
    super.key,
    required this.text,
    required this.isMe,
  });

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: isMe
          ? Alignment.centerRight
          : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(
          horizontal: 12,
          vertical: 5,
        ),
        padding: const EdgeInsets.symmetric(
          horizontal: 14,
          vertical: 10,
        ),
        decoration: BoxDecoration(
          color: isMe
              ? Colors.blue
              : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Text(
          text,
          style: TextStyle(
            color: isMe
                ? Colors.white
                : Colors.black87,
          ),
        ),
      ),
    );
  }
}




25. Chat Screen


class ChatScreen extends StatefulWidget {
  final String chatId;
  final String receiverId;
  final String receiverName;

  const ChatScreen({
    super.key,
    required this.chatId,
    required this.receiverId,
    required this.receiverName,
  });

  @override
  State createState() =>
      _ChatScreenState();
}

class _ChatScreenState
    extends State {
  final messageController =
      TextEditingController();

  void sendMessage() {
    final text =
        messageController.text.trim();

    if (text.isEmpty) {
      return;
    }

    ChatService().sendMessage(
      chatId: widget.chatId,
      senderId: currentUserId,
      receiverId: widget.receiverId,
      text: text,
    );

    messageController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.receiverName),
      ),
      body: Column(
        children: [
          Expanded(
            child: StreamBuilder(
              stream: ChatService()
                  .getMessages(widget.chatId),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return const Center(
                    child:
                        CircularProgressIndicator(),
                  );
                }

                final messages =
                    snapshot.data!.docs;

                return ListView.builder(
                  itemCount: messages.length,
                  itemBuilder: (context, index) {
                    final data =
                        messages[index].data()
                            as Map;

                    return MessageBubble(
                      text: data['text'] ?? '',
                      isMe:
                          data['senderId'] ==
                              currentUserId,
                    );
                  },
                );
              },
            ),
          ),
          MessageInput(
            controller: messageController,
            onSend: sendMessage,
          ),
        ],
      ),
    );
  }
}




26. Message Input


class MessageInput extends StatelessWidget {
  final TextEditingController controller;
  final VoidCallback onSend;

  const MessageInput({
    super.key,
    required this.controller,
    required this.onSend,
  });

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(8),
        child: Row(
          children: [
            Expanded(
              child: TextField(
                controller: controller,
                textInputAction:
                    TextInputAction.send,
                onSubmitted: (_) => onSend(),
                decoration: InputDecoration(
                  hintText: 'Type a message...',
                  border: OutlineInputBorder(
                    borderRadius:
                        BorderRadius.circular(24),
                  ),
                ),
              ),
            ),
            const SizedBox(width: 8),
            IconButton(
              onPressed: onSend,
              icon: const Icon(
                Icons.send,
              ),
            ),
          ],
        ),
      ),
    );
  }
}




27. Chat List Screen


The chat list displays the user's existing conversations.


class ChatListScreen extends StatelessWidget {
  const ChatListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Chats'),
      ),
      body: ListView.builder(
        itemCount: chats.length,
        itemBuilder: (context, index) {
          final chat = chats[index];

          return ListTile(
            leading: CircleAvatar(
              child: Text(
                chat.name[0].toUpperCase(),
              ),
            ),
            title: Text(chat.name),
            subtitle: Text(
              chat.lastMessage,
              maxLines: 1,
              overflow:
                  TextOverflow.ellipsis,
            ),
            trailing: Text(
              chat.time,
            ),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) =>
                      ChatScreen(
                    chatId: chat.id,
                    receiverId:
                        chat.userId,
                    receiverName:
                        chat.name,
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}




28. Display Last Message


The chat document can store the latest message so that the chat list can display a conversation preview without loading the entire message history.


await FirebaseFirestore.instance
    .collection('chats')
    .doc(chatId)
    .update({
  'lastMessage': text,
  'lastMessageTime':
      FieldValue.serverTimestamp(),
});



29. Create a Chat ID


For a one-to-one chat, both users need to reference the same conversation. A deterministic chat ID can be created from the two user IDs.


String createChatId(
  String userId1,
  String userId2,
) {
  final ids = [
    userId1,
    userId2,
  ]..sort();

  return ids.join('_');
}


This approach ensures that the same pair of users produces the same chat ID regardless of who starts the conversation.




30. Online and Offline Status


A chat application can store presence information such as whether a user is online and when the user was last active.


Future updateOnlineStatus(
  String userId,
  bool isOnline,
) async {
  await FirebaseFirestore.instance
      .collection('users')
      .doc(userId)
      .update({
    'isOnline': isOnline,
    'lastSeen':
        FieldValue.serverTimestamp(),
  });
}

For production presence systems, the backend architecture should be designed carefully so that disconnects and network changes are handled reliably.




31. Display Online Status


Row(
  children: [
    Container(
      width: 10,
      height: 10,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: user.isOnline
            ? Colors.green
            : Colors.grey,
      ),
    ),
    const SizedBox(width: 6),
    Text(
      user.isOnline
          ? 'Online'
          : 'Offline',
    ),
  ],
)



32. Last Seen


When a user is offline, the application can display the last active time.


Text(
  user.isOnline
      ? 'Online'
      : 'Last seen recently',
)



33. Typing Indicator


A typing indicator lets the recipient know that the other user is currently typing a message.


class TypingIndicator extends StatelessWidget {
  final bool isTyping;

  const TypingIndicator({
    super.key,
    required this.isTyping,
  });

  @override
  Widget build(BuildContext context) {
    if (!isTyping) {
      return const SizedBox.shrink();
    }

    return const Padding(
      padding: EdgeInsets.all(12),
      child: Align(
        alignment: Alignment.centerLeft,
        child: Text(
          'Typing...',
          style: TextStyle(
            fontStyle: FontStyle.italic,
          ),
        ),
      ),
    );
  }
}




34. Detect Typing


A text controller can be used to detect whether the user has entered text.


messageController.addListener(() {
  final isTyping =
      messageController.text.isNotEmpty;

  updateTypingStatus(
    chatId,
    currentUserId,
    isTyping,
  );
});


In a production application, typing updates should be throttled or debounced so that the backend is not updated on every keystroke.




35. Unread Messages


Unread message counts can be maintained for each conversation.


int unreadCount = 0;

void markMessageAsRead() {
  unreadCount = 0;
}


A backend implementation can store read status or last-read timestamps to synchronize unread counts between devices.




36. Message Timestamp


Each message should contain a timestamp so the interface can display when it was sent.


Text(
  formatTime(
    message.timestamp,
  ),
)

A simple time-formatting function can be created according to the desired application format.




37. Date Separators


Long conversations can be easier to read when messages are grouped by date.


Today
  10:30 AM  Hello
  10:31 AM  How are you?

Yesterday
  08:20 PM  I am fine.

Monday
  09:15 AM  Good morning!




38. Scroll to Latest Message


When a new message is sent, the chat screen should normally scroll toward the latest message.


final ScrollController scrollController =
    ScrollController();

void scrollToBottom() {
  WidgetsBinding.instance
      .addPostFrameCallback((_) {
    if (scrollController.hasClients) {
      scrollController.animateTo(
        scrollController.position.maxScrollExtent,
        duration:
            const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    }
  });
}




39. Image Sharing


A chat application can allow users to select an image, upload it to cloud storage, and send the resulting URL as a message.


final imageMessage = {
  'senderId': currentUserId,
  'receiverId': receiverId,
  'text': imageUrl,
  'type': 'image',
  'timestamp':
      FieldValue.serverTimestamp(),
};



40. Firebase Storage


Firebase Storage can be used to store uploaded images and files.


final storageRef = FirebaseStorage.instance
    .ref()
    .child(
      'chat_images/$fileName',
    );

await storageRef.putFile(file);

final imageUrl =
    await storageRef.getDownloadURL();




41. Image Message Bubble


if (message.type == 'image') {
  return ClipRRect(
    borderRadius:
        BorderRadius.circular(12),
    child: Image.network(
      message.text,
      width: 220,
      height: 220,
      fit: BoxFit.cover,
    ),
  );
}



42. Message Types










Message TypePurpose
textNormal text message.
imageImage message.
fileDocument or file message.
audioAudio message.
videoVideo message.
locationLocation-sharing message.



43. Delete a Message


A message can be deleted when the application's business rules and security rules allow it.


Future deleteMessage(
  String chatId,
  String messageId,
) async {
  await FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .doc(messageId)
      .delete();
}



44. Edit a Message


Message editing can be implemented by updating the message document.


Future editMessage({
  required String chatId,
  required String messageId,
  required String newText,
}) async {
  await FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .doc(messageId)
      .update({
    'text': newText,
    'edited': true,
  });
}



45. Search Users


A search feature can help users find another registered user before starting a conversation.


Future searchUsers(
  String email,
) {
  return FirebaseFirestore.instance
      .collection('users')
      .where(
        'email',
        isEqualTo: email,
      )
      .get();
}



46. Search Conversations


The chat list can also provide a search field to filter conversations locally.


List searchChats(
  List chats,
  String query,
) {
  if (query.trim().isEmpty) {
    return chats;
  }

  return chats.where((chat) {
    return chat.name
        .toLowerCase()
        .contains(
          query.toLowerCase(),
        );
  }).toList();
}




47. Profile Screen


The profile screen can display the user's name, email address, profile photo, status, and account actions.


class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Profile'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            const CircleAvatar(
              radius: 50,
              child: Icon(
                Icons.person,
                size: 50,
              ),
            ),
            const SizedBox(height: 16),
            const Text(
              'John Doe',
              style: TextStyle(
                fontSize: 22,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            const Text(
              '[email protected]',
            ),
            const SizedBox(height: 30),
            ListTile(
              leading: const Icon(
                Icons.edit,
              ),
              title: const Text(
                'Edit Profile',
              ),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(
                Icons.logout,
              ),
              title: const Text(
                'Logout',
              ),
              onTap: () {},
            ),
          ],
        ),
      ),
    );
  }
}




48. Push Notifications


Push notifications can inform users when they receive a new message while they are not actively viewing the chat screen.



  • New message notification

  • New chat notification

  • Call notification

  • Group notification

  • System notification




49. Firebase Cloud Messaging


Firebase Cloud Messaging can be used as part of a push notification system. A typical notification flow is:


Sender
  ↓
Send Message
  ↓
Backend / Cloud Function
  ↓
Firebase Cloud Messaging
  ↓
Receiver Device
  ↓
Notification



50. Chat State Management


Chat applications contain state that may be shared between multiple widgets and screens. Examples include the current user, selected conversation, message list, unread count, online status, and typing state.


class ChatProvider extends ChangeNotifier {
  bool isLoading = false;
  bool isTyping = false;

  void setLoading(bool value) {
    isLoading = value;
    notifyListeners();
  }

  void setTyping(bool value) {
    isTyping = value;
    notifyListeners();
  }
}




51. Provider Setup


void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => ChatProvider(),
      child: const ChatApplication(),
    ),
  );
}



52. Loading State


Loading indicators should be displayed while user information, chats, or messages are being retrieved.


if (isLoading) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}



53. Empty Chat State


When a user has no conversations, the application can display a helpful empty state.


Column(
  mainAxisAlignment:
      MainAxisAlignment.center,
  children: [
    const Icon(
      Icons.chat_bubble_outline,
      size: 80,
    ),
    const SizedBox(height: 16),
    const Text(
      'No conversations yet',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    ),
    const SizedBox(height: 8),
    const Text(
      'Start a conversation with someone.',
    ),
  ],
)



54. Error Handling


Chat applications should handle authentication errors, database errors, network problems, upload failures, and permission errors.


try {
  await ChatService().sendMessage(
    chatId: chatId,
    senderId: senderId,
    receiverId: receiverId,
    text: text,
  );
} catch (error) {
  ScaffoldMessenger.of(context)
      .showSnackBar(
    SnackBar(
      content: Text(
        'Message failed: $error',
      ),
    ),
  );
}



55. Firestore Security Rules Concept


Chat data should not be publicly readable or writable. Security rules should restrict access so that users can access only the documents they are authorized to access.


rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userId} {
      allow read, write:
        if request.auth != null;
    }

    match /chats/{chatId} {
      allow read, write:
        if request.auth != null;
    }
  }
}


The example above is only a simplified learning example. Production security rules should enforce participant-level access rather than granting every authenticated user access to every chat.




56. Chat Privacy



  • Authenticate users before accessing private chat data.

  • Restrict chat access to conversation participants.

  • Protect profile information.

  • Validate message ownership before editing or deleting.

  • Use secure backend rules.

  • Do not expose sensitive credentials in the Flutter application.

  • Use HTTPS and secure backend services.




57. One-to-One Chat Architecture


User A
  ↓
Flutter App
  ↓
Authentication
  ↓
Chat Service
  ↓
Cloud Firestore
  ↓
Real-Time Listener
  ↓
Flutter Chat UI
  ↑
User B
  ↑
Flutter App



58. Real-Time Message Flow


User A Types Message
        ↓
User A Presses Send
        ↓
Message Written to Firestore
        ↓
Firestore Updates
        ↓
Stream Listener Receives Update
        ↓
User B Chat UI Rebuilds
        ↓
User B Sees Message



59. Chat Application Navigation


Splash
  ↓
Login / Register
  ↓
Home
  ├── Chats
  │     ↓
  │   Chat Screen
  │     ↓
  │   Messages
  │
  ├── Search Users
  │     ↓
  │   Start Chat
  │
  └── Profile
        ↓
      Settings



60. Responsive Chat Interface


The chat interface should work on different screen sizes. On a larger screen, the application can display the conversation list and selected chat side by side.


Row(
  children: [
    SizedBox(
      width: 320,
      child: ChatListScreen(),
    ),
    const VerticalDivider(
      width: 1,
    ),
    const Expanded(
      child: ChatScreen(
        chatId: 'chat-id',
        receiverId: 'user-id',
        receiverName: 'User',
      ),
    ),
  ],
)



61. Mobile Chat Layout


Mobile
-------------------------
|       Chat App        |
-------------------------
| Message               |
|             Message   |
| Message               |
|             Message   |
|                       |
-------------------------
| Type a message...  ➤  |
-------------------------



62. Tablet and Desktop Layout


--------------------------------------------
| Conversations |        Chat Screen       |
|---------------|---------------------------|
| User 1        | User Name                |
| User 2        |---------------------------|
| User 3        | Hello                    |
| User 4        |             Hi!          |
| User 5        | How are you?             |
|               |                           |
|               | Type a message...     ➤  |
--------------------------------------------



63. Message Read Status


A chat application can track whether a message has been delivered or read.







StatusMeaning
SentMessage was created by the sender.
DeliveredMessage reached the recipient system/device.
ReadRecipient opened or viewed the message.



64. Read Receipt Example


await FirebaseFirestore.instance
    .collection('chats')
    .doc(chatId)
    .collection('messages')
    .doc(messageId)
    .update({
  'read': true,
  'readAt':
      FieldValue.serverTimestamp(),
});



65. Block User Feature


A production chat application can allow users to block another account.


Future blockUser(
  String currentUserId,
  String blockedUserId,
) async {
  await FirebaseFirestore.instance
      .collection('users')
      .doc(currentUserId)
      .collection('blockedUsers')
      .doc(blockedUserId)
      .set({
    'blockedAt':
        FieldValue.serverTimestamp(),
  });
}



66. Report User Feature


A report feature can allow users to report inappropriate behavior or content. Reports should normally be processed securely on the backend.


Future reportUser({
  required String reporterId,
  required String reportedUserId,
  required String reason,
}) async {
  await FirebaseFirestore.instance
      .collection('reports')
      .add({
    'reporterId': reporterId,
    'reportedUserId': reportedUserId,
    'reason': reason,
    'createdAt':
        FieldValue.serverTimestamp(),
  });
}



67. Chat Search


Search can be implemented for users and conversations. For large datasets, search requirements should be considered when designing the backend data model.


TextField(
  decoration: InputDecoration(
    hintText: 'Search chats',
    prefixIcon:
        const Icon(Icons.search),
    border: OutlineInputBorder(
      borderRadius:
          BorderRadius.circular(12),
    ),
  ),
)



68. Local Data and Offline Experience


A chat application should consider what happens when the device temporarily loses connectivity. Cached data and backend synchronization can provide a better user experience.



  • Display previously loaded conversations.

  • Show connection status.

  • Handle failed message sends.

  • Retry failed operations.

  • Synchronize messages when connectivity returns.




69. Performance Optimization



  • Use ListView.builder for large message lists.

  • Paginate older messages.

  • Avoid rebuilding the complete chat screen unnecessarily.

  • Compress large images before upload when appropriate.

  • Use efficient Firestore queries.

  • Keep listeners scoped to the required data.

  • Dispose controllers when they are no longer needed.

  • Throttle typing-status updates.

  • Load older messages only when required.




70. Pagination for Chat History


Long conversations can contain thousands of messages. Loading the complete history at once can be inefficient. Pagination can load recent messages first and older messages as the user scrolls.


final query = FirebaseFirestore.instance
    .collection('chats')
    .doc(chatId)
    .collection('messages')
    .orderBy('timestamp', descending: true)
    .limit(30);



71. Notifications Flow


Sender
  ↓
Message Sent
  ↓
Backend
  ↓
Check Receiver Status
  ↓
Receiver Offline?
  ↓
Send Push Notification
  ↓
Receiver Device
  ↓
Tap Notification
  ↓
Open Chat Screen



72. Testing the Chat Application










Test TypeExample
Unit TestTest chat ID generation or message formatting.
Widget TestTest message bubbles and input widgets.
Integration TestTest login, chat selection, and message sending.
Authentication TestTest login and registration validation.
Database TestTest message creation and retrieval.
Notification TestTest push notification behavior.



73. Common Problems and Solutions













ProblemPossible Solution
Firebase is not initializedInitialize Firebase before running the application.
Login failsCheck Firebase Authentication configuration and user credentials.
Messages are not appearingCheck Firestore collection paths, queries, security rules, and stream listeners.
Messages are duplicatedCheck listener and message-writing logic.
Chat does not update in real timeVerify that the UI is listening to the correct Firestore stream.
Images fail to uploadCheck Firebase Storage configuration, permissions, and file handling.
Notifications are not receivedCheck FCM configuration, permissions, tokens, and notification handling.
Application becomes slowOptimize listeners, message pagination, images, and widget rebuilding.
Users can access unauthorized chatsReview and strengthen Firestore security rules.



74. Suggested Development Steps



  1. Create the Flutter project.

  2. Configure Firebase.

  3. Add Firebase dependencies.

  4. Initialize Firebase.

  5. Create authentication screens.

  6. Implement registration.

  7. Implement login.

  8. Create the user model.

  9. Create the message model.

  10. Create the chat model.

  11. Design the Firestore database structure.

  12. Implement the chat service.

  13. Create the chat list.

  14. Create the chat screen.

  15. Implement message sending.

  16. Implement real-time message updates.

  17. Add timestamps.

  18. Add online/offline status.

  19. Add typing indicators.

  20. Add unread message counts.

  21. Add read receipts.

  22. Add image sharing.

  23. Add notifications.

  24. Add profile functionality.

  25. Add search.

  26. Add blocking and reporting.

  27. Implement security rules.

  28. Test the application.

  29. Optimize performance.

  30. Prepare the application for release.




75. Best Practices



  • Keep authentication logic separate from UI code.

  • Keep Firestore operations inside service classes.

  • Use models for users, messages, and chats.

  • Use reusable widgets for message bubbles and chat tiles.

  • Use real-time streams carefully.

  • Paginate long conversations.

  • Validate user input.

  • Protect Firestore data with proper security rules.

  • Never expose sensitive credentials in the Flutter client.

  • Handle network and backend errors gracefully.

  • Dispose TextEditingController and ScrollController objects appropriately.

  • Optimize uploaded media.

  • Test authentication and message flows on multiple devices.




76. Advanced Features



  • Group Chat

  • Voice Messages

  • Video Messages

  • Voice Calling

  • Video Calling

  • Message Reactions

  • Message Replies

  • Message Forwarding

  • Message Pinning

  • Disappearing Messages

  • Chat Themes

  • Dark Mode

  • Online Presence

  • Read Receipts

  • Typing Indicators

  • End-to-End Encryption




77. Group Chat Architecture


Group
  ↓
Group Information
  ├── Group Name
  ├── Group Photo
  ├── Admin
  └── Members
        ↓
      Messages
        ├── User A
        ├── User B
        ├── User C
        └── User D



78. Complete Chat Architecture


                  CHAT APPLICATION
                         |
          +--------------+--------------+
          |              |              |
       Auth            Chats          Profile
          |              |
     Login/Register   Chat List
                         |
                    Select User
                         |
                    Chat Screen
                         |
                +--------+--------+
                |        |        |
              Text     Image    File
                |        |        |
                +--------+--------+
                         |
                    Firestore
                         |
                  Real-Time Stream
                         |
                 Receiver Interface
                         |
                    Notification



79. Learning Outcomes


After completing this project, learners should understand how to build a practical Flutter chat application. The project provides experience with Flutter widgets, Dart programming, Firebase Authentication, Firestore, real-time streams, state management, navigation, user profiles, message models, chat services, notifications, media uploads, security rules, responsive layouts, and application testing.




80. Interview Questions



  1. How would you structure a Flutter chat application?

  2. How does Firebase Authentication work with Flutter?

  3. What is Cloud Firestore?

  4. How can Firestore be used for real-time chat?

  5. What is the difference between FutureBuilder and StreamBuilder?

  6. Why is StreamBuilder useful for chat applications?

  7. How would you structure chat messages in Firestore?

  8. How would you generate a unique chat ID?

  9. How would you implement a typing indicator?

  10. How would you implement online/offline status?

  11. How would you implement read receipts?

  12. How would you send push notifications?

  13. How would you upload images in a chat application?

  14. How would you implement message pagination?

  15. How would you protect private chat data?

  16. How would you implement message deletion?

  17. How would you optimize a chat application with thousands of messages?

  18. How would you implement group chat?

  19. What are Firestore security rules?

  20. How would you test a Flutter chat application?




81. Summary


Building a Flutter Chat Application is an excellent practical project for learning real-time application development. The project combines Flutter UI development, Dart programming, authentication, Firestore, real-time streams, state management, navigation, user profiles, message handling, online status, typing indicators, notifications, media uploads, and security.


A beginner version can start with local messages and simple screens. As the application grows, Firebase Authentication, Cloud Firestore, Firebase Storage, push notifications, real-time presence, read receipts, media sharing, group chat, and advanced security can be added.




Learn Flutter with JustAcademy


JustAcademy Flutter Training Course


Register for Flutter Course Demo


whatsapp